﻿using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using Musicman.Common;
using Musicman.Utilities.Configuration.Cron;
using Musicman.Utilities.Configuration.Search;
using System.ComponentModel;
using System.Collections.Specialized;
using Musicman.Common.Entities;
using Musicman.Business;
using System.IO;
using System.Net.Mail;

namespace Musicman.Utilities
{
    /* Search task = [Search Job] * n = run all search jobs
     * Search Job = [Find Files + Synch DB] * n + Clean DB = Find files over all SearchLocations
     * Windows service -> just runs SearchScheduler
     */

    public delegate void SearchJobProgerssEventHandler(SearchJobProgerssChangedEventArgs e);
    public delegate void SearchJobCompletedEventHandler(object sender, SearchJobCompletedEventArgs e);


    /// <summary>
    /// Class that will periodically run scaning for files
    /// </summary>
    public class SearchScheduler
    {
        # region singleton definition
        static readonly SearchScheduler instance = new SearchScheduler();

        public static SearchScheduler Instance
        {
            get
            {
                return instance;
            }
        }
        static SearchScheduler()
        {
            instance.Initialize();
        }
        # endregion

        private bool run = true;
        private Thread cronThread;
        private int loopTime = 60000;  // check each minute
        private DateTime nextDate = DateTime.Today;
        private CronConfigurationSection cronConfiguration;
        private SearchJobsConfigurationSection searchJobsConfiguration;
        private FileFinder fileFinder;
        private HybridDictionary jobStatistics;

        private void Initialize()
        {
            cronThread = new Thread(CreateMonitoringThread);
            cronConfiguration = (CronConfigurationSection)AppSettingsHelper.GetWebConfigSection(Constants.CronSection);
            loopTime = cronConfiguration.CronSettings.LoopMinutes;
            nextDate = cronConfiguration.CronSettings.StartDate;
            searchJobsConfiguration = (SearchJobsConfigurationSection)AppSettingsHelper.GetWebConfigSection(Constants.SearchJobsSection);
            fileFinder = new FileFinder();
            fileFinder.MaxWorkingThreads = searchJobsConfiguration.MaxWorkingThreads;
            fileFinder.MaxWaitTimeMinPerLocation = searchJobsConfiguration.MaxWorkingTimeMin;
            jobStatistics = new HybridDictionary();
            fileFinder.ProgressChanged += new FileFindProgerssEventHandler(fileFinder_ProgressChanged);
            fileFinder.Error += new ErrorEventHandler(fileFinder_Error);
            fileFinder.FileFindCompleted += new FileFindCompletedEventHandler(fileFinder_FileFindCompleted);
            FindNextDate();
        }

        /////////////////////////////////////////////////////////////
        #region Public events

        public event SearchJobProgerssEventHandler ProgressChanged;
        public event ErrorEventHandler Error;
        public event SearchJobCompletedEventHandler SearchJobCompleted;

        #endregion

        void fileFinder_FileFindCompleted(object sender, FileFindCompletedEventArgs e)
        {
            SearchJobConfigurationElement config = (SearchJobConfigurationElement)searchJobsConfiguration.SearchJobs[e.UserState];
            long searchJobId = SearchJobManager.Instance.GetSearchJobId(config.Name, config.Criteria, (byte)config.SearchType, false);
            // clear files that weren't in last search
            int deletedFilesCount = SearchFileManager.Instance.IncreaseAndDeleteNonVisitedFiles(searchJobId, searchJobsConfiguration.DeletAafterSequences);
            // update searchJob
            SearchJobManager.Instance.UpdateSearchJob(searchJobId, e.StartTime, e.EndTime);

            DateTime startTime = DateTime.Now;
            
            SearchJobStatistic overalStats = null;
            lock (jobStatistics.SyncRoot)
            {
                if (jobStatistics[e.UserState] != null)
                {
                    overalStats = (SearchJobStatistic)jobStatistics[e.UserState];
                    jobStatistics.Remove(e.UserState);
                }
            }
            if (overalStats != null) { overalStats.FilesDeleted = deletedFilesCount; }
            SearchJobCompletedEventArgs args = new SearchJobCompletedEventArgs(
                e.Error,
                e.Cancelled,
                e.UserState, 
                overalStats);

            // fire that last search job has finished
            if (this.SearchJobCompleted != null)
            {
                SearchJobCompleted(this, args);
            }
            args.SearchTime = e.EndTime.Ticks - e.StartTime.Ticks;
            if (Boolean.Parse(AppSettingsHelper.GetWebConfigValue(Musicman.Common.Constants.EmailEnabled)))
            {
                SendNotificationMail(args);   
            }

            // run next search job
            if (searchJobsConfiguration.SearchMode == SearchMode.JobAtTheTime)
            {
                int index = searchJobsConfiguration.SearchJobs.IndexOf(config);
                if (index + 1 < searchJobsConfiguration.SearchJobs.Count)
                {
                    config = (SearchJobConfigurationElement)searchJobsConfiguration.SearchJobs[index + 1];
                    RunSearchJob(config);
                }
                else
                {
                    LogManager.LogInfo(String.Format("Next run at -- {0}", nextDate));
                }
            }
            
        }

        void fileFinder_Error(FileFindErrorEventArgs e)
        {
            if (this.Error != null)
            {
                this.Error(e);
            }
        }

        void fileFinder_ProgressChanged(FileFindProgerssChangedEventArgs e)
        {
            DateTime startTime = DateTime.Now;

            if (e.Status == SearchJobStatus.SearchLocatoinTimedOut && this.Error != null)
            {
                FileFindErrorEventArgs ffe = new FileFindErrorEventArgs(
                        new Exception("Search timed out for the following location" + e.SearchLocation), "Search location timed out");
                this.Error(ffe);
            }

            int filesAdded = 0;
            int filesDeleted = 0;

            // process searched data here is the tricky part
            if (e.Status == SearchJobStatus.SearchLocationFinished || e.Status == SearchJobStatus.SearchLocatoinTimedOut)
            {
                SearchJobConfigurationElement currentJob = (SearchJobConfigurationElement)searchJobsConfiguration.SearchJobs[e.UserState];
                long searchJobID = SearchJobManager.Instance.GetSearchJobId(currentJob.Name, currentJob.Criteria, (byte)currentJob.SearchType);

                List<SearchFile> oldFiles = SearchFileManager.Instance.LoadSearchJobFiles(searchJobID, e.SearchLocation);
                
                List<SearchFile> insertList = new List<SearchFile>();
                List<SearchFile> updateList = new List<SearchFile>();
                List<SearchFile> deleteList = new List<SearchFile>();

                for (int i = 0; i < e.Files.Length; i++)
                {
                    FileInfo fi = e.Files[i];
                    SearchFile sf = null;
                    try
                    {
                        sf = oldFiles.Find(delegate(SearchFile sf1) { return (fi.Directory.FullName == sf1.FilePath && fi.Name == sf1.Name); });
                    }
                    catch (Exception ex)
                    {
                        LogManager.LogException(ex);
                    }
                    // file exists in db and also on disk
                    if (sf != null && fi.Exists)
                    {
                        if (sf.PresentSequence != 0)
                        {
                            sf.PresentSequence = 0;
                        }
                        sf.Visited = true;
                        updateList.Add(sf);
                        oldFiles.Remove(sf);
                    }
                    else  
                    {
                        if (fi.Exists)
                        {
                            sf = new SearchFile(fi, searchJobID);
                            insertList.Add(sf);
                        }
                    }
                }
                // files that exist in db and were not find in last search
                for (int i = 0; i < oldFiles.Count; i++)
                {
                    SearchFile sf = oldFiles[i];
                    if ((sf.PresentSequence + 1 > this.searchJobsConfiguration.DeletAafterSequences))
                    {
                        deleteList.Add(sf);
                    }
                    else
                    {
                        sf.PresentSequence += 1;
                        sf.Visited = true;
                        updateList.Add(sf);
                    }
                }

                if (SearchFileManager.Instance.SaveSearchResults(insertList, updateList, deleteList))
                {
                    filesAdded = insertList.Count;
                    filesDeleted = deleteList.Count;
                }
            }

            SearchJobStatistic statistics = new SearchJobStatistic(
                e.Files == null ? 0 : e.Files.Length,
                filesAdded,
                filesDeleted,
                e.ElapsedTime.Ticks,
                DateTime.Now.Ticks - startTime.Ticks);

            lock (jobStatistics.SyncRoot)
            {
                if (jobStatistics[e.UserState] == null)
                {
                    jobStatistics[e.UserState] = statistics;
                }
                else
                {
                    SearchJobStatistic overalStats = (SearchJobStatistic)jobStatistics[e.UserState];
                    overalStats.Add(statistics);
                }
            }

            // fire that process search data has finished
            if (this.ProgressChanged != null)
            {
                TimeSpan processTime = new TimeSpan(DateTime.Now.Ticks - startTime.Ticks);

                SearchJobProgerssChangedEventArgs args = new SearchJobProgerssChangedEventArgs(
                    e.ProgressPercentage,
                    e.UserState,
                    statistics);
                this.ProgressChanged(args);
            }
        }

        private void CreateMonitoringThread()
        {
            while (run)
            {
                try
                {
                    Thread.Sleep(loopTime);
                    if (DateTime.Now > this.nextDate)
                    {
                        FindNextDate();
                        RunSearchTask(searchJobsConfiguration.SearchMode);
                    }
                }
                catch (Exception e)
                {
                    LogManager.LogException("Error in cron job", e);
                }
            }
            
        }

        public void StartCron()
        {
            LogManager.LogInfo("Starting Indexing Service");
            // LogManager.LogInfo(myJobs.ToString());
            LogManager.LogInfo(String.Format("Next run at -- {0}", nextDate));
            cronThread.Start();
        }

        public void StopCron()
        {
            LogManager.LogInfo("Stopping Indexing Service");
            // _cronThread.Abort();
            this.run = false;
            // cancel running tasks if necessery
            StopSearchTask();
        }

        public void FindNextDate()
        {
            while (this.nextDate < DateTime.Now)
            {
                this.nextDate = this.nextDate.AddDays(this.cronConfiguration.CronSettings.Days);
                this.nextDate = this.nextDate.AddHours(this.cronConfiguration.CronSettings.Hours);
                this.nextDate = this.nextDate.AddMinutes(this.cronConfiguration.CronSettings.Minutes);
            }
        }
        
        public void RunSearchTask(SearchMode mode)
        {
            if (mode == SearchMode.JobsAtOnce)
            {
                for (int i = 0; i < searchJobsConfiguration.SearchJobs.Count; i++)
                {
                    SearchJobConfigurationElement searchJobConfig = (SearchJobConfigurationElement)searchJobsConfiguration.SearchJobs[i];
                    RunSearchJob(searchJobConfig);
                }
            }
            else
            {
                SearchJobConfigurationElement searchJobConfig = (SearchJobConfigurationElement)searchJobsConfiguration.SearchJobs[0];
                RunSearchJob(searchJobConfig);
            }
            
            // clear files that doesn't belong to any search job here
            List<long> activeJobIDs = new List<long>();
            for (int i = 0; i < searchJobsConfiguration.SearchJobs.Count; i++)
            {
                SearchJobConfigurationElement searchJobConfig = (SearchJobConfigurationElement)searchJobsConfiguration.SearchJobs[i];
                long jobId = SearchJobManager.Instance.GetSearchJobId(searchJobConfig.Name, searchJobConfig.Criteria, (byte)searchJobConfig.SearchType);
                if (jobId != -1)
                {
                    activeJobIDs.Add(jobId);
                }
            }
            if (activeJobIDs.Count != 0)
            {
                // delete inactive jobs and all it's data
                SearchJobManager.Instance.ClearInactiveJobs(activeJobIDs);
            }
        }

        public void StopSearchTask()
        {
            foreach (SearchJobConfigurationElement config in searchJobsConfiguration.SearchJobs)
            {
                fileFinder.CancelAsync(config.Name);
            }
        }

        public void SendNotificationMail(SearchJobCompletedEventArgs args)
        {
            try
            {
                List<User> recipients = UserManager.Instance.LoadMailSubscribers();
                string mailTemplate = "";
                using (StreamReader sr = new StreamReader(File.Open(AppDomain.CurrentDomain.BaseDirectory + @"\mail_template.htm", FileMode.Open, FileAccess.Read)))
                {
                    mailTemplate = sr.ReadToEnd();
                }
                TimeSpan searchtime = new TimeSpan(args.SearchTime);
                TimeSpan processtime = new TimeSpan(args.Processtime);
                MailMessage email = new MailMessage();
                email.From = new MailAddress("musicmanservices@musicman.com", "Musicman");
                StringBuilder addresses = new StringBuilder("");
                foreach (User recipient in recipients)
                {
                    addresses.Append(recipient.Emailaddress + ",");
                }
                email.To.Add(addresses.ToString());
                email.Subject = String.Format("musicman job finished {0}", DateTime.Now);
                email.Body = String.Format(mailTemplate,
                    DateTime.Now,
                    searchtime,
                    args.UserState,
                    args.FilesFound,
                    args.FilesAdded,
                    args.FilesDeleted);
                email.IsBodyHtml = true;
                MailSender.Instance.SendMail(email);
            }
            catch (Exception ex)
            {
                LogManager.LogException("Unable to send mail", ex);
            }
        }

        public void RunSearchJob(SearchJobConfigurationElement config)
        {
            try
            {
                long jobId = SearchJobManager.Instance.GetSearchJobId(config.Name, config.Criteria, (byte)config.SearchType);
                SearchFileManager.Instance.SetVisited(jobId, false);
                fileFinder.SearchFilesAsync(config, searchJobsConfiguration.SearchLocations.ToStringList(), config.Name);
            }
            catch (ArgumentException ex)
            {
                LogManager.LogException("Search job with following name already running", ex);
                // Search job with following name already running
            }
        }
    }

    #region Arguments and helper classes
    public class SearchJobProgerssChangedEventArgs : ProgressChangedEventArgs
    {
        private int filesFound;
        private int filesAdded;
        private int filesDeleted;
        private long searchTime;
        private long processtime;

        public SearchJobProgerssChangedEventArgs(int progressPercentage, object userState, SearchJobStatistic statistics)
            : base(progressPercentage, userState)
        {
            this.filesFound = statistics.FilesFound;
            this.filesAdded = statistics.FilesAdded;
            this.filesDeleted = statistics.FilesDeleted;
            this.searchTime = statistics.SearchTime;
            this.processtime = statistics.ProcessTime;
        }
    }

    public class SearchJobCompletedEventArgs : AsyncCompletedEventArgs
    {
        private int filesFound;
        private int filesAdded;
        private int filesDeleted;
        private long searchTime;
        private long processtime;
       
        public SearchJobCompletedEventArgs(Exception error, bool cancelled, object state, SearchJobStatistic statistics) 
            : base(error, cancelled, state)
        {
            this.filesFound = statistics.FilesFound;
            this.filesAdded = statistics.FilesAdded;
            this.filesDeleted = statistics.FilesDeleted;
            this.searchTime = statistics.SearchTime;
            this.processtime = statistics.ProcessTime;
        }

        public int FilesFound
        {
            get { return filesFound; }
            set { filesFound = value; }
        }

        public int FilesAdded
        {
            get { return filesAdded; }
            set { filesAdded = value; }
        }
        public int FilesDeleted
        {
            get { return filesDeleted; }
            set { filesDeleted = value; }
        }
        public long SearchTime
        {
            get { return searchTime; }
            set { searchTime = value; }
        }
        public long Processtime
        {
            get { return processtime; }
            set { processtime = value; }
        }
    }

    public class SearchJobStatistic
    {
        private int filesFound;
        private int filesAdded;
        private int filesDeleted;
        private long searchTime;
        private long processTime;

        public SearchJobStatistic(int filesFound, int filesAdded, int filesDeleted, long searchTime, long processTime)
        {
            this.filesFound = filesFound; 
            this.filesAdded = filesAdded;
            this.filesDeleted = filesDeleted;
            this.searchTime = searchTime;
            this.processTime = processTime;
        }
        
        public void Add(SearchJobStatistic statisteics)
        {
            this.filesFound += statisteics.filesFound;
            this.filesAdded += statisteics.filesAdded;
            this.filesDeleted += statisteics.filesDeleted;
            this.searchTime += statisteics.searchTime;
            this.processTime += statisteics.processTime;
        }

        public int FilesFound
        {
            get { return filesFound; }
            set { filesFound = value; }
        }
        public int FilesAdded
        {
            get { return filesAdded; }
            set { filesAdded = value; }
        }
        public int FilesDeleted
        {
            get { return filesDeleted; }
            set { filesDeleted = value; }
        }
        public long SearchTime
        {
            get { return searchTime; }
            set { searchTime = value; }
        }
        public long ProcessTime
        {
            get { return processTime; }
            set { processTime = value; }
        }
    }
    #endregion
}
